from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
from google.appengine.api.urlfetch import fetch, ResponseTooLargeError

from google.appengine.ext import db
from google.appengine.api import memcache

import pickle
import hashlib
import logging
import time

import config

class Resource(db.Model):
    status_code = db.IntegerProperty()
    content = db.BlobProperty()
    headers = db.BlobProperty()
    
ADMIN_STATUS = {
    0: 'OK',
    1: 'Not found',
    2: 'Error on storage layer',
    3: 'Bad hash',
    255: 'General error',
}
    
def renderAdminXml(status, data = {}):
    xmldata = []
    for d in data.keys():
        xmldata.append("<%s>%s</%s>" % (d, data[d], d))

    xml = '''<?xml version="1.0" encoding="UTF-8"?>
        <result>
            <status>%d</status>
            <message>%s</message>
            <data>
            %s
            </data>
        </result>''' % (status, ADMIN_STATUS[status], "\n".join(xmldata))
    return xml

class InvalidateResource(webapp.RequestHandler):
    def get(self, hash, path):
        self.response.headers['Content-Type'] = 'text/xml'
        
        # 3e48635acc1f772426d53912d05364aad77c8194
        salt = hashlib.sha1(''.join([config.SECRET_KEY, path])).hexdigest()
        
        logging.info('Invalidating %s' % path)
        
        status = 255
        if hash == salt:    # Hash is OK
            if config.USE_MEMCACHE:
                ret = memcache.delete(path)
                if ret == 2:  # Resource was deleted successfully
                    status = 0
                elif ret == 1:  # Resource was not found
                    status = 1
                else:       # Some error occurred
                    status = 2
                    
            if config.USE_DATASTORE:
                 res = Resource.get_by_key_name(path)
                 if not res: status = 1 # Resource not found
                 try:
                     res.delete()
                 except:
                     status = 2 # Some error occurred
                 else:
                     status = 0 # Succesfully done
                     
        else:   # Hash is wrong, nothing happen
            status = 3
        
        self.response.out.write(renderAdminXml(status))

class InvalidateAllResources(webapp.RequestHandler):
    def get(self, hash):
        self.response.headers['Content-Type'] = 'text/xml'
        
        # 904b489c218d334b79d1a68eb3d4930e2119bda3        
        salt = hashlib.sha1(config.SECRET_KEY).hexdigest()
        
        logging.info('Invalidating all resources')
                
        status = 255
        if hash == salt:    # Hash is OK
            ret = memcache.flush_all()
            if ret:  # Resource was deleted successfully
                status = 0
            else:
                status = 2 
        else:   # Hash is wrong, nothing happen
            status = 3
        
        self.response.out.write(renderAdminXml(status))

class RefreshResource(webapp.RequestHandler):
    def get(self, hash, path):
        self.response.headers['Content-Type'] = 'text/xml'
        
        # 3e48635acc1f772426d53912d05364aad77c8194
        salt = hashlib.sha1(''.join([config.SECRET_KEY, path])).hexdigest()
        
        logging.info('Refreshing %s' % path)

        # Invalidate old copy
        inv = InvalidateResource()
        # Throw out response using fresh Response() instance
        inv.initialize(self.request, webapp.Response())
        inv.get(hash, path)
        
        # Reload resource
        get = GetResource()
        get.initialize(self.request, webapp.Response())
        get.head(path)
        
        status = 0
        self.response.out.write(renderAdminXml(status))
        
class Stats(webapp.RequestHandler):
    def get(self, hash):
        self.response.headers['Content-Type'] = 'text/xml'

        salt = hashlib.sha1(config.SECRET_KEY).hexdigest()
        
        logging.info('Reading statistics')
                
        status = 255
        data = {}
        if hash == salt:    # Hash is OK
            data = memcache.get_stats()
            status = 0

        else:   # Hash is wrong, nothing happen
            status = 3
        
        self.response.out.write(renderAdminXml(status, data))

class GetResource(webapp.RequestHandler):
    def head(self, path):
        # Do the same as for GET, but don't send any content
        self.get(path)

    def saveDatastore(self, res):
        # Store resource to datastore
        if config.USE_DATASTORE:
            res.put()

    def saveMemcache(self, res):
        # Store resource to memcache
        if config.USE_MEMCACHE:
            self.load_headers(res)
            memcache.set(res.key().name(), res)
            
    def load_headers(self, res):
        # Unpickle headers from res.headers to res._headers_dict,
        # so _headers_dict can be stored into memcache
        try:
            res._headers_dict
        except AttributeError:
            res._headers_dict = pickle.loads(res.headers)
            logging.info("Unpickling headers")
        return res._headers_dict
    
    def get(self, path):
        # Create path to URL
        if self.request.query_string:
            path = '%s?%s' % (path, self.request.query_string)      
    
        # Files outside META_URL are forbidden
        if not path.startswith(config.MEDIA_URL):
            self.redirect(''.join([config.SOURCE_URL, path]), permanent=True)
            return
        
        res = None
        #res = Resource.gql("WHERE path = :path", path=path)
        
        # Lookup into memcache
        if config.USE_MEMCACHE:
            res = memcache.get(path)
            if res:
                # We found object in cache, lets serve it...
                logging.info('Using memcache for %s' % path)
        
        # Memcache is empty or not used, try to load from datastore
        if not res and config.USE_DATASTORE:
            res = Resource.get_by_key_name(path)
            if res:
                # We found object in datastore, lets serve it...
                logging.info('Using datastore for %s' % path)
                self.saveMemcache(res)
            
        # Fetching from original source
        if not res:
            res = Resource(key_name=path)             
            logging.info('Downloading source file for %s' % path)
            
            # Force UrlFetch to load fresh copy of resource
            timestamp = '?%d' % time.time()
            
            try:    
                # We don't have data in cache, so lets download data from source location
                content = fetch(''.join([config.SOURCE_URL, path, timestamp]), follow_redirects=True) 
            
                # Fake ResponseTooLargeError exception on dev server
                #if len(content.content) > 1000000:
                #    raise ResponseTooLargeError(content)
            except ResponseTooLargeError:
                # Resource is too large for GAE, redirect browsers to original URL
                res.status_code = 301 # Permanent redirect
            else:    
                # Download was successful
                res.status_code = content.status_code
                res.content = content.content

            res._headers_dict = {}
            for h in ['content-type', 'expires', 'etag', 'cache-control']:
                if h in content.headers: res._headers_dict[h] =  content.headers[h]
            res.headers = pickle.dumps(res._headers_dict)

            # Can be resource stored in caches?
            if res.status_code in config.STATUS_STORE:
                self.saveDatastore(res)
                self.saveMemcache(res)
                
        # Append additional headers to response object 
        # _headers is not stored in datastore, but is parsed and saved into memcache
        headers = self.load_headers(res)
        for h in headers: self.response.headers[h] = headers[h]
            
        # Look at etag headers. If browser have actual version of file,
        # send 304 Not Modified instead file content (saves bandwidth)
        if 'if-none-match' in self.request.headers and 'etag' in res.headers and \
                self.request.headers['if-none-match'] == res.headers['etag']:
            self.response.set_status(304) # Not modified
            return
        
        # Send result to browser
        if res.status_code == 301:
            # Redirect by source site or when resource is over size quota
            self.redirect('%s%s' % (config.SOURCE_URL, path), permanent=True)
        else:
            self.response.set_status(res.status_code)
            self.response.out.write(res.content)
        
application = webapp.WSGIApplication([
                                      ('/_sys/invalidate/([a-z0-9]+)(/.*)', InvalidateResource),
                                      ('/_sys/invalidate-all/([a-z0-9]+)[/]?', InvalidateAllResources),
                                      ('/_sys/refresh/([a-z0-9]+)(/.*)', RefreshResource),
                                      ('/_sys/stats/([a-z0-9]+)[/]?', Stats),
                                      ('(/.*)', GetResource),
                                      ], debug=False)

if __name__ == "__main__":
    run_wsgi_app(application)
